// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: junglekim, jhkim

#pragma once

#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <stack>

#include "coin2/strategy/util/moving_window.h"
#include "coin2/strategy/vmm/vmm_order_controller.h"

using coin2::strategy::order_executor::LayeringLevels;
using coin2::strategy::util::TimeWeightedMovingAverage;

namespace coin2::strategy::vmm {

class VmmDeltaHedgeOrderController : public VmmOrderController {
 public:
  VmmDeltaHedgeOrderController(
      const ::nlohmann::json& oc_config,
      const bool& dry_run,
      const int64_t& start_time,
      const bool& verbose)
      : VmmOrderController(oc_config, dry_run, verbose), start_time_(start_time) {
    timer_map_.push_back({TimerType::FEED, ""});
    for (auto& config : configs_.items()) {
      std::string symbol = config.key();
      reserves_[symbol] = configs_[symbol]["reserve"];
      pos_offsets_[symbol] = config.value()["pos_offset"];
      min_twap_weights_[symbol] = config.value()["min_twap_weight"];
      CHECK_GE(pos_offsets_[symbol], 0);
      const bool razor_on = config.value()["razor_on"];
      const double min_spread_bp = config.value()["min_spread_bp"];
      const double bandwidth_bp = config.value()["bandwidth_bp"];
      const double logspace_start = config.value()["logspace_start"];
      const double logspace_end = config.value()["logspace_end"];
      const int logspace_num = config.value()["logspace_num"];
      const int layer_num = config.value()["layer_num"];
      const double grid_period_hour = config.value()["grid_period_hour"];
      const double real_delta_hour = config.value()["calc_real_delta_hour"];
      const int market_window_sec = config.value()["market_window_sec"];
      const int bbo_interval_ms = config.value()["bbo_interval_ms"];
      const double rfr = config.value()["risk_free_rate"];
      const double volatility = config.value()["volatility"];
      const double pda_pct = config.value()["pos_diff_allowance_pct"];
      const int random_timer_sec = config.value()["random_timer_sec"];
      const int dh_agg_window_sec = config.value()["dh_agg_window_sec"];
      const double dh_agg_window_limit_quote = config.value()["dh_agg_window_limit_quote"];
      const int razor_num =
          config.value().contains("razor_num") ? config.value()["razor_num"].get<int>() : layer_num;
      const int dh_agg_interval_sec = config.value().contains("dh_agg_interval_sec")
                                          ? config.value()["dh_agg_interval_sec"].get<int>()
                                          : 10;
      const double dh_agg_max_quote = config.value().contains("dh_agg_max_quote")
                                          ? config.value()["dh_agg_max_quote"].get<double>()
                                          : 10.0;
      const double hedge_max_random_diff =
          config.value().contains("hedge_max_random_diff")
              ? config.value()["hedge_max_random_diff"].get<double>()
              : 0.0;
      const double hedge_qty_tolerance_bp =
          config.value().contains("hedge_qty_tolerance_bp")
              ? config.value()["hedge_qty_tolerance_bp"].get<double>()
              : 0.0;
      const double deco_max_random_diff = config.value().contains("deco_max_random_diff")
                                              ? config.value()["deco_max_random_diff"].get<double>()
                                              : 0.0;
      const double deco_qty_tolerance_bp =
          config.value().contains("deco_qty_tolerance_bp")
              ? config.value()["deco_qty_tolerance_bp"].get<double>()
              : 0.0;
      const bool is_randomised_post = config.value().contains("is_randomised_post")
                                          ? config.value()["is_randomised_post"].get<bool>()
                                          : false;
      const int cancel2order_ratio = config.value().contains("cancel2order_ratio")
                                         ? config.value()["cancel2order_ratio"].get<int>()
                                         : std::numeric_limits<int>::max() - 1;
      CHECK_GE(cancel2order_ratio, 1);
      std::optional<double> qty_cancel_pct =
          config.value().contains("qty_cancel_pct")
              ? std::optional(config.value()["qty_cancel_pct"].get<double>())
              : std::nullopt;
      std::optional<double> midp_hint = config.value().contains("midp_hint")
                                            ? std::optional(config.value()["midp_hint"])
                                            : std::nullopt;
      std::optional<double> midp_hint_duration_sec =
          config.value().contains("midp_hint_duration_sec")
              ? std::optional(config.value()["midp_hint_duration_sec"].get<double>())
              : std::nullopt;
      const bool midp_hint_force = config.value().contains("midp_hint_force")
                                       ? config.value()["midp_hint_force"].get<bool>()
                                       : false;
      std::vector<double> strikes;
      std::vector<int64_t> expiries;
      std::vector<int> call_nums;
      for (auto& contract : config.value()["contracts"].items()) {
        if (contract.key() == "owner") {
          continue;
        }
        const auto& strike_num = contract.value()["strike_price_num"].get<std::string>();
        const auto& strike_denom = contract.value()["strike_price_denom"].get<std::string>();
        const double strike_price = std::stod(strike_num) / std::stod(strike_denom);

        strikes.push_back(strike_price);
        expiries.push_back(Readable2Epoch(contract.value()["expiry"]));
        call_nums.push_back(contract.value()["call_num"]);
      }
      deltahedgers_[symbol] = new DeltaHedger(
          symbol,
          CallInfo{&strikes, &expiries, &call_nums},
          LayeringParam{min_spread_bp / 2,
                        min_spread_bp / 2 + bandwidth_bp,
                        layer_num,
                        random_timer_sec},
          DeltaCalcParam{logspace_start,
                         logspace_end,
                         logspace_num,
                         grid_period_hour,
                         real_delta_hour,
                         rfr,
                         volatility},
          ExecutorParam{hedge_qty_tolerance_bp,
                        qty_cancel_pct,
                        is_randomised_post,
                        cancel2order_ratio},
          pda_pct,
          razor_on,
          razor_num,
          dh_agg_interval_sec,
          dh_agg_max_quote,
          market_window_sec,
          bbo_interval_ms,
          hedge_max_random_diff,
          dh_agg_window_sec,
          dh_agg_window_limit_quote);

      midphints__[symbol] =
          new MidpHint(start_time_, midp_hint_duration_sec, midp_hint, midp_hint_force);

      double last_end_bp = 0.0;
      if (config.value().contains("decos")) {
        int window_sec = config.value()["decos"]["defense"]["window_sec"];
        int duration_sec = config.value()["decos"]["defense"]["duration_sec"];
        double imbalance_scale =
            config.value()["decos"]["defense"].contains("imbalance_scale")
                ? config.value()["decos"]["defense"]["imbalance_scale"].get<double>()
                : 0.0;
        for (auto& deco : config.value()["decos"].items()) {
          CHECK(config.value()["decos"].contains("defense"));
          if (deco.key() == "defense") continue;
          double start_bp = deco.value()["start_bp"];
          CHECK_GT(start_bp, last_end_bp);
          const double end_bp = deco.value()["end_bp"];
          last_end_bp = end_bp;
          const int layer_num = deco.value()["layer_num"];
          const double value = deco.value()["value"];
          const double twap_decay = deco.value()["twap_decay"];
          const double filled_decay = deco.value()["filled_decay"];
          bool buy_on = false, sell_on = false;
          if (deco.value().contains("on_off")) {
            buy_on = deco.value()["on_off"]["buy"];
            sell_on = deco.value()["on_off"]["sell"];
          }
          decorators_[symbol].push_back(Decorator(
              deco.key(),
              LayeringParam{start_bp, end_bp, layer_num, random_timer_sec},
              ExecutorParam{deco_qty_tolerance_bp,
                            qty_cancel_pct,
                            is_randomised_post,
                            cancel2order_ratio},
              value,
              imbalance_scale,
              twap_decay,
              filled_decay,
              buy_on,
              sell_on,
              duration_sec,
              window_sec,
              deco_max_random_diff));
        }
      }

      CHECK(!(
          config.value().contains("centreback_sell") && config.value().contains("centreback_buy")));
      if (config.value().contains("centreback_sell")) {
        centrebacks_[symbol] = new Centreback(
            -1,
            config.value()["centreback_sell"]["trigger_pct"],
            config.value()["centreback_sell"]["min_price_pct"],
            config.value()["centreback_sell"]["max_price_pct"],
            config.value()["centreback_sell"]["value"],
            config.value()["centreback_sell"]["layer_num"],
            config.value()["centreback_sell"]["window_hour"]);
      }
      if (config.value().contains("centreback_buy")) {
        centrebacks_[symbol] = new Centreback(
            1,
            config.value()["centreback_buy"]["trigger_pct"],
            config.value()["centreback_buy"]["min_price_pct"],
            config.value()["centreback_buy"]["max_price_pct"],
            config.value()["centreback_buy"]["value"],
            config.value()["centreback_buy"]["layer_num"],
            config.value()["centreback_buy"]["window_hour"]);
      }

      dn_agged_[symbol] = false;
      if (config.value().contains("dn_agg_max_quote")) {
        dellneutaggs_[symbol] = new DelNeutAgg(
            config.value()["dn_agg_order_freq_sec"],
            config.value()["dn_agg_thold_bp"],
            config.value()["dn_agg_max_bp"],
            config.value()["dn_agg_max_quote"],
            config.value()["dn_agg_pos_diff_cap_pct"],
            config.value()["dn_agg_book_num"],
            config.value()["dn_agg_random_intensity"],
            dry_run_);
      }
    }
  }

  void AvoidArbi(const std::string symbol, const VmmPriceResult result) {
    results_[symbol] = result;
    max_bidp_[symbol] = result.native_ask.first.value();
    min_askp_[symbol] = result.native_bid.first.value();
    for (const auto& [sym, res] : results_) {
      if (sym == symbol) continue;
      max_bidp_[symbol] = std::min(max_bidp_[symbol], res.native_ask.first.value());
      min_askp_[symbol] = std::max(min_askp_[symbol], res.native_bid.first.value());
    }

    if (result.priced_bid.first.has_value() && result.priced_ask.first.has_value()) {
      max_bidp_[symbol] = std::min(max_bidp_[symbol], result.priced_ask.first.value());
      min_askp_[symbol] = std::max(min_askp_[symbol], result.priced_bid.first.value());
    }
  }

  void UpdateFairPrices(const int64_t ts, const std::string symbol, const VmmPriceResult prices) {
    const double msb = deltahedgers_[symbol]->min_spread_bp;
    const double native_bidp = prices.native_bid.first.value();
    const double native_askp = prices.native_ask.first.value();
    const double real_pos =
        executors_[symbol]->GetPosition() - reserves_[symbol] - pos_offsets_[symbol];
    auto [twap_bid, twap_ask] = deltahedgers_[symbol]->twap_bbo();
    double twap_mid = (twap_bid + twap_ask) * 0.5;
    auto [hf_bidp, hf_askp] = deltahedgers_[symbol]->HedgeFairPrices(real_pos);

    fair_bidp_[symbol] = twap_mid;
    fair_askp_[symbol] = twap_mid;
    hedge_bidp_[symbol] = hf_bidp;
    hedge_askp_[symbol] = hf_askp;
    if (prices.ref_priced_midp.has_value()) {
      fair_bidp_[symbol] = prices.ref_priced_midp.value();
      fair_askp_[symbol] = prices.ref_priced_midp.value();
    }
    if (hf_bidp > native_askp || hf_askp < native_bidp) deltahedgers_[symbol]->update_needed = true;

    fair_bidp_[symbol] = fair_bidp_[symbol] / (1 + (msb * 0.5) * 1e-4) < native_askp
                             ? fair_bidp_[symbol]
                             : native_askp;
    fair_askp_[symbol] = fair_askp_[symbol] * (1 + (msb * 0.5) * 1e-4) > native_bidp
                             ? fair_askp_[symbol]
                             : native_bidp;
    if (!decorators_[symbol].empty()) {
      fair_bidp_[symbol] =
          fair_bidp_[symbol] / (1 + decorators_[symbol].front().start_bp * 1e-4) > max_bidp_[symbol]
              ? max_bidp_[symbol] * (1 + decorators_[symbol].front().start_bp * 1e-4)
              : fair_bidp_[symbol];
      fair_askp_[symbol] =
          fair_askp_[symbol] * (1 + decorators_[symbol].front().start_bp * 1e-4) < min_askp_[symbol]
              ? min_askp_[symbol] / (1 + decorators_[symbol].front().start_bp * 1e-4)
              : fair_askp_[symbol];
    }
    const double min_price = std::min(max_bidp_[symbol], min_askp_[symbol]);
    const double max_price = std::max(max_bidp_[symbol], min_askp_[symbol]);

    if (fair_bidp_[symbol] < min_price) {
      const double twap_bid_weight =
          min_twap_weights_[symbol] + std::clamp(
                                          std::exp(native_bidp / twap_bid - 1.0) - 1.0,
                                          0.0,
                                          1 - min_twap_weights_[symbol]);
      fair_bidp_[symbol] = twap_bid_weight * twap_bid + (1 - twap_bid_weight) * min_price;
    }

    if (fair_askp_[symbol] > max_price) {
      const double twap_ask_weight =
          min_twap_weights_[symbol] + std::clamp(
                                          std::exp(twap_ask / native_askp - 1.0) - 1.0,
                                          0.0,
                                          1 - min_twap_weights_[symbol]);
      fair_askp_[symbol] = twap_ask_weight * twap_ask + (1 - twap_ask_weight) * max_price;
    }

    if (fair_bidp_[symbol] == max_bidp_[symbol] || fair_askp_[symbol] == min_askp_[symbol]) {
      deltahedgers_[symbol]->update_needed = true;
    }
    if (midphints__[symbol]->is_on(ts)) {
      auto fair_midp = midphints__[symbol]->get();
      if (!midphints__[symbol]->is_forced() &&
          (fair_midp < native_bidp || fair_midp > native_askp)) {
        fair_midp = twap_mid;
      }
      fair_bidp_[symbol] = std::min(fair_midp, max_bidp_[symbol]);
      fair_askp_[symbol] = std::max(fair_midp, min_askp_[symbol]);
    }
  }

  void onPricerUpdate(
      const std::unordered_map<std::string, VmmPriceResult>& result,
      const int64_t& ts) final {
    for (const auto& [symbol, prices] : result) {
      if (!os_[symbol] || !os_[symbol]->is_ready()) return;
      if (!prices.native_bid.first.has_value() || !prices.native_ask.first.has_value()) return;
      if (!deltahedgers_[symbol]->executor) {
        deltahedgers_[symbol]->InitExecutor(executors_[symbol]->config(), ts, driver_);
      }
      for (auto& decorator : decorators_[symbol]) {
        if (!decorator.executor) decorator.InitExecutor(executors_[symbol]->config(), ts, driver_);
      }
      if (!(centrebacks_.find(symbol) == centrebacks_.end()) && !centrebacks_[symbol]->executor)
        centrebacks_[symbol]->InitExecutor(executors_[symbol]->config(), ts, driver_);
      deltahedgers_[symbol]->executor->UpdateState(ts);
      AvoidArbi(symbol, prices);
      const double real_pos =
          executors_[symbol]->GetPosition() - reserves_[symbol] - pos_offsets_[symbol];
      const double midp = (prices.native_bid.first.value() + prices.native_ask.first.value()) * 0.5;
      const double cb_qty = centrebacks_.find(symbol) == centrebacks_.end()
                                ? 0.0
                                : centrebacks_[symbol]->sell_qty - centrebacks_[symbol]->buy_qty;
      if (!(centrebacks_.find(symbol) == centrebacks_.end()))
        centrebacks_[symbol]->Update(midp, ts);
      deltahedgers_[symbol]->UpdateBbo(
          prices.native_bid.first.value(),
          prices.native_ask.first.value(),
          ts);
      deltahedgers_[symbol]->UpdateStatus(ts, real_pos, midp, cb_qty);
      for (auto& decorator : decorators_[symbol]) {
        decorator.UpdateImbalance(real_pos, deltahedgers_[symbol]->desired_pos, cb_qty);
      }
    }
    for (auto& [symbol, delneutagg] : dellneutaggs_) {
      if (result.find(symbol) == result.end()) continue;
      if (!delneutagg->executor)
        delneutagg->InitExecutor(executors_[symbol]->config(), ts, driver_);
      dn_agged_[symbol] = delneutagg->Update(ts, result.at(symbol), deltahedgers_.at(symbol));
    }

    for (const auto& [symbol, prices] : result) {
      UpdateFairPrices(ts, symbol, prices);
    }
    for (const auto& [symbol, prices] : result) {
      if (dn_agged_[symbol]) continue;
      const double& native_bid = prices.native_bid.first.value();
      const double& native_ask = prices.native_ask.first.value();
      const double& midp = (native_bid + native_ask) * 0.5;
      for (auto& decorator : decorators_[symbol]) {
        decorator.UpdateTimeOnly(ts);
      }
      double arbi_bidp = std::min(max_bidp_[symbol], min_askp_[symbol]);
      double arbi_askp = std::max(max_bidp_[symbol], min_askp_[symbol]);
      if (deltahedgers_[symbol]->update_needed) {
        deltahedgers_[symbol]->UpdateSquad(
            ts,
            hedge_bidp_[symbol],
            hedge_askp_[symbol],
            native_bid,
            native_ask,
            arbi_bidp,
            arbi_askp);
      }
      if (deltahedgers_[symbol]->update_needed || last_midp_[symbol] != midp) {
        for (auto& decorator : decorators_[symbol]) {
          const auto& [twap_bidp, twap_askp] = deltahedgers_[symbol]->twap_bbo();
          const double max_twap_diff =
              std::max({native_bid / twap_bidp, twap_askp / native_ask, 1.0});
          decorator.UpdateSquad(ts, fair_bidp_[symbol], fair_askp_[symbol], max_twap_diff);
        }
      }
      if (!dry_run_) {
        deltahedgers_[symbol]->SubmitLevels(ts);
        for (auto& decorator : decorators_[symbol]) {
          decorator.SubmitLevels(ts);
        }
      }
      last_midp_[symbol] = midp;
      deltahedgers_[symbol]->update_needed = false;
    }
  }

  void onAccountInfo(const OrderUpdate& upd) final {
    for (auto& [_, executor] : executors_) {
      executor->onAccountInfo(upd);
    }
    for (auto& [_, centreback] : centrebacks_) {
      if (centreback->executor) centreback->executor->onAccountInfo(upd);
    }
    for (auto& [_, deltahedger] : deltahedgers_) {
      if (deltahedger->executor) deltahedger->executor->onAccountInfo(upd);
    }
    for (auto& [_, decorators] : decorators_) {
      for (auto& decorator : decorators) {
        if (decorator.executor) decorator.executor->onAccountInfo(upd);
      }
    }
  }

  void onAccountOrder(const OrderUpdate& upd) final {
    const auto symbol = GetSymbol(upd.product());
    executors_[symbol]->onAccountOrder(upd);
    for (auto& [sym, centreback] : centrebacks_) {
      if (symbol == sym && centreback->executor) centreback->executor->onAccountOrder(upd);
    }
    if (deltahedgers_[symbol]->executor) deltahedgers_[symbol]->executor->onAccountOrder(upd);
    for (auto& decorator : decorators_[symbol]) {
      if (decorator.executor) decorator.executor->onAccountOrder(upd);
    }
    auto oe = upd.product_order_info().event();
    const auto& tag = oe.tag();
    if (oe.type() == coin::proto::OrderEvent::ORDER_FILLED)
      deltahedgers_[symbol]->UpdateFilledInfo(upd);
    const std::string pass = "AggArb|DeltaNeutAgg";
    if (pass.find(tag) != std::string::npos) return;
    if (oe.type() == coin::proto::OrderEvent::ORDER_FILLED)
      deltahedgers_[symbol]->update_needed = true;
    const auto& ts = upd.timestamp();
    if (oe.type() == coin::proto::OrderEvent::ORDER_FILLED && tag == "CB") {
      const auto& side = oe.trade_side();
      if (side == coin::proto::TRADE_BUY_SIDE) {
        centrebacks_[symbol]->buy_qty += oe.fill_qty();
        centrebacks_[symbol]->buy_ts = ts;
      } else if (side == coin::proto::TRADE_SELL_SIDE) {
        centrebacks_[symbol]->sell_qty += oe.fill_qty();
        centrebacks_[symbol]->sell_ts = ts;
      }
    }
    if (std::all_of(tag.begin(), tag.end(), ::isdigit)) {
      const auto deco_idx = std::stoi(tag);
      if (oe.type() == coin::proto::OrderEvent::ORDER_FILLED && deco_idx > 0) {
        const auto& side = oe.trade_side();
        const auto& price = oe.order_price();
        const auto& qty = oe.fill_qty();
        decorators_[symbol][deco_idx - 1].UpdateTrade(ts, price, qty, side);
      }
    }
  }

 protected:
  int64_t start_time_;

  std::unordered_map<std::string, double> max_bidp_;
  std::unordered_map<std::string, double> min_askp_;
  std::unordered_map<std::string, double> fair_bidp_;
  std::unordered_map<std::string, double> fair_askp_;
  std::unordered_map<std::string, double> hedge_bidp_;
  std::unordered_map<std::string, double> hedge_askp_;
  std::unordered_map<std::string, double> last_midp_;
  std::unordered_map<std::string, DeltaHedger*> deltahedgers_;
  std::unordered_map<std::string, MidpHint*> midphints__;
  std::unordered_map<std::string, std::vector<Decorator>> decorators_;
  std::unordered_map<std::string, Centreback*> centrebacks_;
  std::unordered_map<std::string, DelNeutAgg*> dellneutaggs_;
  std::unordered_map<std::string, VmmPriceResult> results_;
  std::unordered_map<std::string, double> reserves_;
  std::unordered_map<std::string, double> pos_offsets_;
  std::unordered_map<std::string, double> min_twap_weights_;
  std::unordered_map<std::string, bool> dn_agged_;
  std::unordered_map<std::string, int64_t> last_filled_ts_;
};

}  // namespace coin2::strategy::vmm